目前的登入頁面是跳轉到 Keycloak 預設的登入頁,所以會出現樣式跟 NextJs App 不一致的情形,keycloak 本身是有自訂樣式的功能,但與 React 大相徑庭,要調成一致不知道要費多少工。
幸好有工具可以處理這個問題,Keycloakify 提供使用 React 做成 Keycloak theme 的功能,能夠使用 tailwind, styledComponent 等樣式工具,也能用 Mui 等元件庫來製作。
首先將 keycloakify 專案複製到本地並安裝
git clone https://github.com/keycloakify/keycloakify-starter
cd keycloakify-starter
yarn install
再來為了開發方便,先開啟 storybook 好看到改動後的樣子。
一開始還沒有任何 story ,先用指令選擇想要顯示在 storybook 中的頁面,再開啟 storybook,這邊以 login 頁面為例
npx keycloakify add-story
npm run storybook
再來如果想要能夠整個元件做替換的話,要先將頁面 eject 後才能進行編輯。
npx keycloakify eject-page
執行後就會出現一個檔案 src/login/pages/Login.tsx
可以編輯來改動 login 頁面。
另外選完後會看到提示要將自訂頁面元件做切替。
// src/login/KcPage.tsx
// ...
+const Login = lazy(() => import("./pages/Login"));
export default function KcPage(props: { kcContext: KcContext; }) {
// ...
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
// ...
+ case "login.ftl": return (
+ <Login
+ {...{ kcContext, i18n, classes }}
+ Template={Template}
+ doUseDefaultCss={true}
+ />
+ );
default: return <Fallback /* .. */ />;
}
})()}
</Suspense>
);
}
接著先安裝 Mui
yarn add @mui/material @emotion/react @emotion/styled
來試著將 Sign in 按鈕替換成 Mui 元件,先找到原本的按鈕部分
<input
tabIndex={7}
disabled={isLoginButtonDisabled}
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
/>
替換成
<Button
variant="contained"
disabled={isLoginButtonDisabled}
name="login"
id="kc-login"
type="submit"
fullWidth
>
{msgStr("doLogIn")}
</Button>
就能看到按鈕變成 Mui 的樣子了。
目前看到的背景圖是 body 跟 html 標籤上的 class 的影響,不在 login 頁面的範圍內,所以要從更上層的元件來修改。
先呈現修改後的結果
// src/login/KcPage.tsx
import { Suspense, lazy } from "react";
import type { ClassKey } from "keycloakify/login";
import type { KcContext } from "./KcContext";
import { useI18n } from "./i18n";
import DefaultPage from "keycloakify/login/DefaultPage";
import Template from "keycloakify/login/Template";
import { createTheme, ThemeProvider } from "@mui/material";
import { tss } from "tss-react/mui";
const UserProfileFormFields = lazy(
() => import("keycloakify/login/UserProfileFormFields")
);
const Login = lazy(() => import("./pages/Login"));
const doMakeUserConfirmPassword = true;
const theme = createTheme({
palette: {
mode: "dark"
}
});
export default function KcPage(props: { kcContext: KcContext }) {
return (
<ThemeProvider theme={theme}>
<KcPageContext {...props} />
</ThemeProvider>
);
}
function KcPageContext(props: { kcContext: KcContext }) {
const { kcContext } = props;
const { i18n } = useI18n({ kcContext });
const { classes } = useStyles();
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "login.ftl":
return (
<Login
{...{ kcContext, i18n, classes }}
Template={Template}
doUseDefaultCss={true}
/>
);
default:
return (
<DefaultPage
kcContext={kcContext}
i18n={i18n}
classes={classes}
Template={Template}
doUseDefaultCss={true}
UserProfileFormFields={UserProfileFormFields}
doMakeUserConfirmPassword={doMakeUserConfirmPassword}
/>
);
}
})()}
</Suspense>
);
}
const useStyles = tss.create(
({ theme }) =>
({
kcHtmlClass: {},
kcBodyClass: {
backgroundColor: theme.palette.background.default
}
}) satisfies { [key in ClassKey]?: unknown }
);
主要是要將 kcHtmlClass, kcBodyClass 這兩個 class 清空或覆寫。
先到這邊,接著再來將結果打包後套用回 Keycloak。